﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Windows;
using ChatRoom.Protocol;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Threading;
using TingSLsocket;

namespace ChatRoom.ViewModel
{
    public interface IChatRoomView
    {
        void ShowErrorMsg(string message);
    }

    public sealed class DisplayMsg
    {
        public const string Self = "Me";

        public string Source { get; set; }
        public string Time { get; set; }
        public string Content { get; set; }
    }

    public sealed class ChatAgent : JobAgent
    {
        private readonly ChatRoomViewModel _viewModel;
        public ChatAgent(ChatRoomViewModel viewModel)
            : base(Guid.Empty)
        {
            _viewModel = viewModel;
        }

        public override Tuple<int, object> Handle(int cmd, object parm)
        {
            switch (cmd)
            {
                case Constants.CmdSendChat:
                    return Tuple.Create(cmd, parm);// forward directly

                case Constants.CmdRecvChat:
                    _viewModel.ShowMessage((ChatMessage)parm);
                    return Tuple.Create(-1, (object)null);

                case Constants.CmdRefreshClients:
                    _viewModel.RefreshPeers((ClientInfo[])parm);
                    return Tuple.Create(-1, (object)null);

                default:
                    throw new ArgumentException("invalid command received");
            }
        }
    }

    public class ChatRoomViewModel : ViewModelBase, IDisposable
    {
        // -------------------------------------- //
        #region "member fields"

        public IChatRoomView View { get; set; }
        private readonly RpcClient _rpcClient;
        private IDictionary<Guid, ClientInfo> _id2ClientInfoMap;

        #endregion

        // -------------------------------------- //
        #region "bindable properties"

        private string _txtToSend;
        public string TxtToSend
        {
            get { return _txtToSend; }
            set
            {
                if (_txtToSend == value) return;
                _txtToSend = value;
                RaisePropertyChanged("TxtToSend");
            }
        }

        private ClientInfo[] _clientInfos;
        public ClientInfo[] ClientInfos
        {
            get { return _clientInfos; }
            set
            {
                if (_clientInfos == value) return;
                _clientInfos = value;
                _id2ClientInfoMap = _clientInfos.ToDictionary(c => c.Id);
                RaisePropertyChanged("ClientInfos");
            }
        }

        private bool _isConnected;
        public bool IsConnected
        {
            get { return _isConnected; }
            set
            {
                if (_isConnected == value) return;
                _isConnected = value;
                RaisePropertyChanged("IsConnected");
                CmdSendChat.RaiseCanExecuteChanged();
            }
        }

        // to let the ListBox always scroll to display the last item
        private DisplayMsg _currentItem;
        public DisplayMsg CurrentItem
        {
            get {return _currentItem; }
            set
            {
                if(_currentItem == value) return;
                _currentItem = value;
                RaisePropertyChanged("CurrentItem");
            }
        }

        public ObservableCollection<DisplayMsg> ChatMessages { get; private set; }

        #endregion

        // -------------------------------------- //
        #region "commands"

        public RelayCommand CmdSendChat { get; private set; }
        private void OnSendChat()
        {
            var newmsg = new ChatMessage { Time = DateTime.Now, Content = this.TxtToSend };
            _rpcClient.Request(Constants.CmdSendChat, newmsg);
            AppendMessage(newmsg);
        }

        #endregion

        // -------------------------------------- //
        #region "constructor and destructor"

#if SILVERLIGHT

        public ChatRoomViewModel(IChatRoomView view)
            : this(Application.Current.Host.Source.DnsSafeHost, view)
        {

        }

#endif

        public ChatRoomViewModel(string host, IChatRoomView view)
        {
            // -------------------- initialize the RPC client
            EndPoint svrendpnt = new DnsEndPoint(host, Constants.ServerPort);
            var agent = new ChatAgent(this);
            _rpcClient = new RpcClient(svrendpnt, Constants.ClientBufSize, agent);
            _rpcClient.ConnectedEvt += (sender, _) => Dispatch(() => { IsConnected = true; });
            _rpcClient.ClosedEvt += (_, sockerror) => OnError(new SocketException((int)sockerror));
            _rpcClient.ErrorEvt += (_, ex) => OnError(ex);

            // -------------------- initialize commands
            this.CmdSendChat = new RelayCommand(OnSendChat, () => IsConnected);

            // -------------------- 
            this.NeedSwitchContext = true;
            View = view;
            IsConnected = false;
            ChatMessages = new ObservableCollection<DisplayMsg>();
        }

        public void Dispose()
        {
            _rpcClient.Dispose();
        }

        #endregion

        // -------------------------------------- //
        #region "public API"

        public void OnLoaded()
        {
            if (IsInDesignMode)
            {
                DateTime timebase = DateTime.Now;
                for (int index = 0; index < 5; index++)
                {
                    var msg = new DisplayMsg
                                  {
                                      Source = string.Format("source-{0}", index),
                                      Time = timebase.AddSeconds(index).ToShortTimeString(),
                                      Content = string.Format("message-{0}", index)
                                  };
                    this.ChatMessages.Add(msg);
                }

                this.ClientInfos = (from index in Enumerable.Range(1, 3)
                                    select new ClientInfo
                                               {
                                                   Host = string.Format("host{0}", index),
                                                   Port = index
                                               }).ToArray();
            }
            else
            {
                _rpcClient.Connect();
            }
        }

        public bool NeedSwitchContext { get; set; }

        public void ShowMessage(ChatMessage msg)
        {
            Dispatch(() => AppendMessage(msg));
        }

        public void RefreshPeers(ClientInfo[] clientInfos)
        {
            Dispatch(() => { ClientInfos = clientInfos; });
        }

        #endregion

        // -------------------------------------- //
        #region "private helpers"

        private void OnError(Exception ex)
        {
            Dispatch(() =>
            {
                IsConnected = false;
                string msg = string.Format("disconnected with server due to: {0}", ex.Message);
                View.ShowErrorMsg(msg);
            });
        }

        private void Dispatch(Action action)
        {
            // although DispatcherHelper uses "CheckAccess" to check
            // however, it only check whether the calling thread is the same thread
            // where the Dispatcher is created
            // even in our test, the above rule will never satisfy
            // so it will always call "BeginInvoke"
            // since test program doesn't have message queue
            // so the action will never be invoked
            if (NeedSwitchContext)
            {
                DispatcherHelper.CheckBeginInvokeOnUI(action);
            }
            else
            {
                action();
            }
        }

        private void AppendMessage(ChatMessage msg)
        {
            DisplayMsg display = new DisplayMsg
            {
                Time = msg.Time.ToShortTimeString(),
                Content = msg.Content
            };

            if (msg.ClientId == Guid.Empty)
            {
                display.Source = DisplayMsg.Self;
            }
            else
            {
                ClientInfo info = null;
                if (_id2ClientInfoMap.TryGetValue(msg.ClientId, out info))
                {
                    display.Source = string.Format("<{0}:{1}>", info.Host, info.Port);
                }
            }

            if (display.Source != null)
            {
                this.ChatMessages.Add(display);
                CurrentItem = display;
            }
        }

        #endregion
    }
}
